在介紹 redux-observable 之前,先稍微理解一下 RxJS 的基本概念,RxJS 可以幫助我們解決很多非同步事件的疑難雜症,它提供了一套完整的非同步解決方案。
Web 存取各種資源大多是非同步(Async)的,隨著網頁需求的複雜化,我們所寫的 JavaScript 就有各種針對非同步行為的寫法,如下圖所示。
使用 Callback、Generator 或是 Promise 甚至是新的語法糖 async/await,但隨著應用需求愈來愈複雜,撰寫非同步的程式碼仍然非常困難。
假如想監聽點擊事件(click event),但點擊一次之後不再監聽。
一般的寫法如下
const handler = (e) => {
console.log(e);
document.body.removeEventListener('click', handler);
// 結束監聽
}
// 註冊監聽
document.body.addEventListener('click', handler);
改寫用 RxJS
Rx.Observable
.fromEvent(document.body, 'click') // 註冊監聽
.take(1) // 只取一次
.subscribe(console.log);
透過 RxJS 的 API 來做資料操作,可以發現程式碼比較清楚易懂,每個步驟在做什麼事情。RxJS 是一套藉由 Observable sequences 來組合非同步行為和事件基礎程序的 Library!(你可以想像它是用來處理非同步行為的 Lodash)
由於 RxJS 可通用於所有非同止的操作,所以當專案的非同步操作是複雜精密的,就可以考慮引入 RxJS 來解決問題。
如果想深入理解 RxJS 的話,就不要錯過這個鐵人賽冠軍系列文章 - 30 天精通 RxJS
先假定大家都有 RxJS 的基本觀念,那就來看看如何應用 redux-observable 解決 Redux 非同步的問題。
回顧一下 Redux Middleware 所扮演的角色,前面的文章有介紹 React Middleware 的 基本運作原理。
epics 是 redux-observable 核心概念
function (
action$: Observable<Action>,
state$: StateObservable<State>
): Observable<Action>;
React Redux 使用 redux-observable 來實現 RxJS
npm install rxjs –save
npm install redux-observable –save
這邊我們沿用前面的範例,把 setFilterAsync 改寫成 使用 redux-observable 的寫法。
// 新增一個 ActionTypes
export const SET_FILTER_DELAY = "SET_FILTER_DELAY";
// store/actions/index.js
import { SET_FILTER, SET_FILTER_DELAY, ...} from "./actionTypes";
export const setFilter = (filter) => {
return {
type: SET_FILTER,
filter
};
};
// 新增一個 action creator 讓它可以指定 delay 多久
export const setFilterDelay = (filter, delay) => {
return {
type: SET_FILTER_DELAY,
filter,
delay
};
};
action$.pipe(ofType(FIRST, SECOND, THIRD)) // FIRST or SECOND or THIRD
// store/epics/index.js
import { SET_FILTER_DELAY } from "../actions/actionTypes";
import { setFilter } from "../actions";
import { ofType } from "redux-observable";
import { interval } from "rxjs";
import { map, delayWhen } from "rxjs/operators";
export const setFilterDelayEpic = (action$) =>
action$.pipe(
// filter(({ type }) => type === SET_FILTER_DELAY),
ofType(SET_FILTER_DELAY),
// 使用 delayWhen 搭配 interval 做參數自訂的 delay
delayWhen(({ delay }) => interval(delay)),
// delay 後才執行 setFilter(filter)
map(({ filter }) => setFilter(filter))
);
import { createStore, applyMiddleware } from "redux";
import reducers from "./reducers";
import { combineEpics, createEpicMiddleware } from "redux-observable";
import { setFilterDelayEpic } from "./epics";
export default function configureStore() {
// createEpicMiddlewarec會將epic函數轉為redux中間件
const epicMiddleware = createEpicMiddleware();
const enhancers = applyMiddleware(epicMiddleware);
const store = createStore(reducers, enhancers);
// combineEpics會將參數中的epic函數合併在一起
const epics = combineEpics(setFilterDelayEpic);
// 這段要放在 createStore() 後執行
epicMiddleware.run(epics);
return store;
}
// src/index.js
import configureStore from "./store";
...
const store = configureStore();
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
<Provider store={store}>
<App />
</Provider>
);
import { setFilterDelay } from "../store/actions";
...
dispatch(setFilterDelay(filterTitle, 2000))}
這時候切換 Filter 會發現資料會延遲我們指定的秒數才做出變化。
完整程式碼操作:https://codesandbox.io/s/react-todomvc-redux-observable-delay-4n6y1j
一樣沿用前面的範例,把 fetchTodosAsync 改寫成 使用 redux-observable 的寫法。
export const SET_FILTER_DELAY = "SET_FILTER_DELAY";
// 新增 ActionTypes 如下
export const FETCH_TODOS = "FETCH_TODOS";
export const FETCH_TODOS_SUCCESS = 'FETCH_TODOS_SUCCESS';
// store/actions/index.js
import {
...,
SET_FILTER,
SET_FILTER_DELAY,
FETCH_TODOS,
FETCH_TODOS_SUCCESS
} from "./actionTypes";
export const fetchTodos = (data) => {
return {
type: FETCH_TODOS,
data
};
};
// 新增一個 action creator 讓 fetch api 成功後可以對應執行
export const fetchTodosSuccess = (data) => {
return {
type: FETCH_TODOS_SUCCESS,
data
};
};
// store/reducers/todosReducer.js
import {FETCH_TODOS_SUCCESS, ...} from "./actionTypes";
switch (action.type) {
...
// reducer 裡不要有 FETCH_TODOS 的對應,而是 FETCH_TODOS_SUCCESS
case FETCH_TODOS_SUCCESS:
const newTodos = action.data.map(
({ id, title, completed }) => {
return {
id,
text: title,
completed
};
});
return [...newTodos];
...
}
import { ofType } from "redux-observable";
import { ajax } from "rxjs/ajax";
import { map, delayWhen, mergeMap } from "rxjs/operators";
const url = "https://jsonplaceholder.typicode.com/users/1/todos";
export const fetchTodosEpic = (action$) =>
action$.pipe(
// filter(({ type }) => type === FETCH_TODOS),
ofType(FETCH_TODOS),
mergeMap((action) =>
ajax.getJSON(url).pipe(map((response) => fetchTodosSuccess(response)))
)
);
import { setFilterDelayEpic, fetchTodosEpic } from "./epics";
...
// combineEpics會將參數中的epic函數合併在一起
const epics = combineEpics(setFilterDelayEpic, fetchTodosEpic);
// 這段要放在 createStore() 後執行
epicMiddleware.run(epics);
import { setFilterDelay, fetchTodos } from "../store/actions";
...
dispatch(fetchTodos());
完整程式碼操作:https://codesandbox.io/s/react-todomvc-redux-observable-api-pd2u6g
參考 官網說明,可以再進一步修改之前串接 API 的範例,追加一個 fetchTodosCancel 的功能。
這邊有稍微調整 todos 狀態的結構如下,所以相對應的 reducer 及 元件取得 state 的方式稍微不同。
const initialState = {
data: [],
isLoading: false,
error: false
};
這裡不多贅述,大家可以直接去看 調整後的結果。
export const SET_FILTER_DELAY = "SET_FILTER_DELAY";
export const FETCH_TODOS = "FETCH_TODOS";
export const FETCH_TODOS_SUCCESS = "FETCH_TODOS_SUCCESS";
// 新增 ActionTypes 如下
export const FETCH_TODOS_CANCEL = "FETCH_TODOS_CANCEL";
// store/actions/index.js
import {
...,
SET_FILTER,
SET_FILTER_DELAY,
FETCH_TODOS,
FETCH_TODOS_SUCCESS,
FETCH_TODOS_CANCEL
} from "./actionTypes";
...
export const fetchTodosCancel = () => {
return {
type: FETCH_TODOS_CANCEL
};
};
// store/reducers/todosReducer.js
import {FETCH_TODOS_SUCCESS, ...} from "./actionTypes";
switch (action.type) {
// 這裡要加 FETCH_TODOS 用來做 isLoading 狀態的控制
case FETCH_TODOS:
return Object.assign({}, state, {
data: [...state.data],
isLoading: true,
error: null
});
case FETCH_TODOS_SUCCESS:
const newData = action.data.map(({ id, title, completed }) => {
return {
id,
text: title,
completed
};
});
return Object.assign({}, state, {
data: [...newData],
isLoading: false,
error: null
});
// 加上 FETCH_TODOS_CANCEL
case FETCH_TODOS_CANCEL:
return Object.assign({}, state, {
data: [],
isLoading: false,
error: null
});
}
import { ofType } from "redux-observable";
import { ajax } from "rxjs/ajax";
// 增加一個 takeUntil 的 operator
import { ..., takeUntil } from "rxjs/operators";
export const fetchTodosEpic = (action$, { dispatch }) =>
action$.pipe(
// filter(({ type }) => type === FETCH_TODOS),
ofType(FETCH_TODOS),
mergeMap(() =>
ajax.getJSON(url).pipe(
map((response) => {
return fetchTodosSuccess(response);
}),
// 如果有執行 FETCH_TODOS_CANCEL 就取消 Fetch
takeUntil(action$.pipe(ofType(FETCH_TODOS_CANCEL)))
)
)
);
import { setFilterDelay, fetchTodos } from "../store/actions";
...
<span
style={{ zIndex: 10 }}
onClick={() => {
dispatch(fetchTodosCancel());
}}
>
Load Cancel
</span>
完整程式碼操作:https://codesandbox.io/s/react-todomvc-redux-observable-api-cancellation-loading-d9i9nm
在前面幾篇介紹 Redux 的文章,可以發現建置 Redux 不是一件容易的工作,所以 Redux 官方後來提供了一個工具包 - Redux Toolkit,它是一個可以幫助你更有效率撰寫 Redux 的一個 library,它提供了一些 API 讓你更方便的建立 Store、Actions 和 Reducers。
https://redux-observable.js.org/
https://ithelp.ithome.com.tw/articles/10230156
https://www.facebook.com/photo?fbid=5130624853619445&set=gm.3014867732115273
https://www.slideshare.net/newstory0113/why-reduxobservable
https://pjchender.dev/webdev/note-without-redux/
https://dev.to/andrejnaumovski/async-actions-in-redux-with-rxjs-and-redux-observable-efg